19. 实现 Transformer Decoder 组件#

19.1. Transformer Decoder 概览#

Transformer 的 Decoder 部分是整个 Transformer 架构中负责生成的组件。Decoder 的目标是根据已有的输入和编码器的输出,逐个时间步地预测后面的序列。

image.png

标准的 Transformer Decoder 由多个相同的解码器层(块)堆叠而成。

image.png

每一个解码器层内部主要由以下三个子层构成:

  • 带掩码多头注意力层:这是 Decoder 自带的注意力机制。它允许 Decoder 关注当前已生成的所有词,但由于是自回归生成,它必须通过掩码来阻止关注未来的词,需要使用掩码,确保预测位置智能看到当前位置之前的词,这种机制称为“因果掩码”(Causal Mask)。

  • 交叉注意力层:这是 Decoder 与 Encoder 交互的桥梁。该层使用 Decoder 自身的 Query,去查询 Encoder 输出的 Key 和 Value,让 Decoder 能够关注到输入序列中与当前生成相关的信息。

  • 前馈网络层:与 Encoder 中的完全相同,是一个全连接网络,对注意力层输出的数据进行非线性变换。

此外,每个子层都使用了残差连接和层归一化,让模型在训练时能够更稳定地收敛。

image.png

19.1.1. 解码器的解码过程#

Transformer 解码器的解码有以下步骤:

  1. 首先,我们将解码器的输入转换为嵌入矩阵,然后将位置编码加入其中,并将其作为输入送入底层的解码器。

  2. 解码器收到输入,并将其发送给 带掩码的自注意力层,生成融合了上下文信息的 Token 特征矩阵。

  3. 然后,将特征矩阵和编码器输出的特征值作为 交叉注意力层 的输入,并再次生成新的融合了编码器输入信息的 Token 特征矩阵。

  4. 把新得到的特征矩阵作为输入,送入前馈网络层,进一步在更精细的维度上重组融合,输出最终的 Token 特征矩阵。

  5. 然后,我们用相同的方法,把从底层解码器得到的特征矩阵作为输入,将其送入下一层的解码器。

  6. 经过多轮这样的处理,最终得到目标句的特征矩阵,我们可以用一个输出层将其映射到目标词汇表的大小,得到每个位置上的预测分布。

19.1.2. 解码器和编码器的差异#

前馈网络层、残差连接和层归一化的作用和实现方式与 Transformer Encoder 中的完全相同,这里就不再重新介绍。

Decoder与Encoder主要有两点核心差异:

  • 掩码机制

    • Encoder 使用普通注意力,在计算某个词时可以“看到”句子中所有的词(包括前后文),这是双向的。

    • Decoder 则使用掩码注意力。在训练和推理时,通过一个上三角矩阵将未来的信息遮盖掉,让模型只能依赖当前位置之前的信息,这是单向的。这确保了模型在预测下一个词时不会“偷看”到正确答案。

  • 交叉注意力

    • Encoder 是独立的,只处理自己的输入,没有引入外部信息的模块。

    • Decoder 则多了一个交叉注意力层,专门用来吸收 Encoder 提取的特征,实现从输入序列到输出序列的信息传递。

本章重点是实现 Transformer 中的掩码机制。Transformer 中使用了三种不同的掩码:

  • 填充掩码(Padding Mask):这种Mask用于处理输入序列长度不一致的问题。在自注意力机制中,较短的序列会被填充为相同的长度,而这些填充的位置会通过设置为无穷小(负无穷大)来忽略掉,从而不影响模型的计算结果。

  • 因果掩码(Causal Mask):这种Mask用于防止在解码器生成输出时,当前位置的信息被后续位置的信息影响。具体来说,它会在自注意力计算中遮盖掉当前位置之后的信息,确保生成的单词只依赖于之前已经生成的单词。

  • 交叉掩码(Cross Mask):在编码器-解码器交叉注意力层中,过滤编码器输出的填充部分(如补零位置)。确保解码器仅关注编码器输出的实际内容,忽略填充符号。

19.2. 填充掩码(Pad Mask)#

Padding Mask 是一种用于处理变长序列数据的掩码机制,主要用于在 Transformer 模型中区分输入序列中的真实数据和填充数据。

Padding Mask 是一个布尔矩阵,用于指示输入序列中哪些位置是填充的(即用 0 填充的位置),哪些位置是实际数据(即非填充的位置)。其主要目的是在自注意力机制中屏蔽掉填充部分,确保模型不会将这些无意义的填充数据纳入计算,从而提高模型的效率和准确性。

1+100 填充到长度 7 为例,它的填充掩码矩阵如下图所示,其中灰色的位置表示填充位置(用 PAD 填充的位置),绿色的位置表示实际数据位置(即非填充的位置):

image.png

具体实现方式如下: - 在生成 Padding Mask 时,通常会根据输入序列的长度动态生成一个与输入序列形状相同的矩阵。例如,对于一个长度为 seq_len 的序列,Padding Mask 的形状可以是 [batch_size, seq_len],其中 batch_size 是批次大小。 - 具体实现上,可以通过比较输入序列中的每个位置是否为填充符号(如 PAD),然后将填充位置标记为 0,非填充位置标记为 1

在 Transformer 的自注意力机制中,Padding Mask 被用来屏蔽掉填充部分的位置。具体来说,当计算注意力分数时,填充位置的注意力权重会被设置为一个非常大的负数(如负无穷),这样在经过 softmax 归一化后,这些位置的权重几乎为零,从而不会对模型的输出产生影响。

image.png

image.png

此外,在模型训练过程中,Padding Mask 还可以防止填充数据对模型学习产生干扰,确保模型只关注实际输入的有效部分。

19.2.1. 填充掩码的代码实现#

from torch import nn


def create_cross_attn_pad_mask(query_ids, key_ids, pad_token_id):
    """
    创建填充掩码矩阵,用于屏蔽padding位置的注意力
    
    :param query_ids: 查询ID序列,形状为[batch_size, query_len]
    :param key_ids: 键ID序列,形状为[batch_size, key_len]
    :param pad_token_id: 填充标记ID,用于识别哪些位置是padding
    :return: 填充掩码矩阵,形状为[batch_size, query_len, key_len],其中1表示有效位置,0表示padding位置
    """
    batch_size, query_len = query_ids.size()
    batch_size, key_len = key_ids.size()

    # 生成Key序列的padding掩码:ne(pad_token_id)返回非填充位置为True,填充位置为False(ne 是 "not equal" 的缩写,表示"不等于"操作)
    # unsqueeze(1)在第1维添加维度,形状变为[batch_size, 1, key_len]
    # byte类型(torch.uint8)只需要1个字节的存储空间,对于只包含0和1的掩码矩阵,使用byte类型可以显著减少内存占用
    key_attn_pad_mask = key_ids.data.ne(pad_token_id).unsqueeze(1).byte()

    # 将Key的填充mask信息,复制给每一个Query
    return key_attn_pad_mask.expand(batch_size, query_len, key_len)

19.2.2. 生成填充掩码矩阵#

from dsxllm.transformer.tokenizer import get_tokenizer

question = "1+100"
tokenizer = get_tokenizer()
encoded_result = tokenizer(question, max_length=7, padding=True, return_tensors=True)
print("encoded_result:\n", encoded_result["input_ids"], "\n")

encoder_input_ids = encoded_result["input_ids"].unsqueeze(0)
print("encoder_input_ids:\n", encoder_input_ids, "\n")

# 生成被关注的Key的填充mask矩阵,0表示填充,1表示原始token
pad_token_id = tokenizer.pad_token_id
key_attn_pad_mask = create_cross_attn_pad_mask(query_ids=encoder_input_ids,
                                               key_ids=encoder_input_ids,
                                               pad_token_id=pad_token_id)
print("key_attn_pad_mask:\n", key_attn_pad_mask, "\n")
encoded_result:
 tensor([ 1, 10,  1,  0,  0, 12, 12]) 

encoder_input_ids:
 tensor([[ 1, 10,  1,  0,  0, 12, 12]]) 

key_attn_pad_mask:
 tensor([[[1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0]]], dtype=torch.uint8) 

19.2.3. 在 Self-Attention 层中使用填充掩码#

from dsxllm.transformer.layers import SingleHeadAttention
from dsxllm.util import print_red

# 超参设置
d_model = 4
num_embeddings = tokenizer.vocab_size

# 1️⃣ 创建 Token Embedding 层
embed_layer = nn.Embedding(num_embeddings=num_embeddings, embedding_dim=d_model)

# 创建输入数据的嵌入表示
encoder_input = embed_layer(encoder_input_ids)
print_red("输入数据:")
print(encoder_input, "\n")

# 2、生成被关注的Key的填充mask矩阵,0表示填充,1表示原始token
pad_token_id = tokenizer.pad_token_id
key_attn_pad_mask = create_cross_attn_pad_mask(
    encoder_input_ids, encoder_input_ids, pad_token_id
)

# 初始化单头自注意力层
attn = SingleHeadAttention(d_model, is_print=True)
print_red("单头自注意力层:")
print(attn, "\n")

print("-" * 100, "\n")

# 使用单头自注意力层进行计算
hidden_states, _ = attn(
    q_input=encoder_input,
    k_input=encoder_input,
    v_input=encoder_input,
    attn_mask=key_attn_pad_mask,
)
输入数据:
tensor([[[ 0.1085,  1.6926,  1.0728,  0.5288],
         [-0.6277, -2.4309, -1.6768,  1.6910],
         [ 0.1085,  1.6926,  1.0728,  0.5288],
         [-1.6693, -0.1664,  0.6599,  0.1362],
         [-1.6693, -0.1664,  0.6599,  0.1362],
         [-0.6359, -0.6651,  0.0025, -2.1165],
         [-0.6359, -0.6651,  0.0025, -2.1165]]], grad_fn=<EmbeddingBackward0>) 

单头自注意力层:
SingleHeadAttention(
  (q_proj): Linear(in_features=4, out_features=4, bias=False)
  (k_proj): Linear(in_features=4, out_features=4, bias=False)
  (v_proj): Linear(in_features=4, out_features=4, bias=False)
) 

---------------------------------------------------------------------------------------------------- 

【步骤1】计算query、key和value向量 

【步骤2】计算注意力分数
掩码前的scores:
 tensor([[[-0.1930,  0.2184, -0.1930,  0.2117,  0.2117,  0.2241,  0.2241],
         [-0.0022, -0.0814, -0.0022, -0.5068, -0.5068, -0.2059, -0.2059],
         [-0.1930,  0.2184, -0.1930,  0.2117,  0.2117,  0.2241,  0.2241],
         [ 0.0169, -0.1553,  0.0169,  0.6253,  0.6253,  0.4403,  0.4403],
         [ 0.0169, -0.1553,  0.0169,  0.6253,  0.6253,  0.4403,  0.4403],
         [ 0.3107, -0.3603,  0.3107,  0.4642,  0.4642,  0.0828,  0.0828],
         [ 0.3107, -0.3603,  0.3107,  0.4642,  0.4642,  0.0828,  0.0828]]],
       grad_fn=<DivBackward0>) 

【步骤 2-2】根据掩码重设无需关注的注意力分数
掩码后的scores:
 tensor([[[-1.9301e-01,  2.1841e-01, -1.9301e-01,  2.1167e-01,  2.1167e-01,
          -1.0000e+09, -1.0000e+09],
         [-2.1652e-03, -8.1440e-02, -2.1652e-03, -5.0683e-01, -5.0683e-01,
          -1.0000e+09, -1.0000e+09],
         [-1.9301e-01,  2.1841e-01, -1.9301e-01,  2.1167e-01,  2.1167e-01,
          -1.0000e+09, -1.0000e+09],
         [ 1.6892e-02, -1.5526e-01,  1.6892e-02,  6.2533e-01,  6.2533e-01,
          -1.0000e+09, -1.0000e+09],
         [ 1.6892e-02, -1.5526e-01,  1.6892e-02,  6.2533e-01,  6.2533e-01,
          -1.0000e+09, -1.0000e+09],
         [ 3.1067e-01, -3.6031e-01,  3.1067e-01,  4.6423e-01,  4.6423e-01,
          -1.0000e+09, -1.0000e+09],
         [ 3.1067e-01, -3.6031e-01,  3.1067e-01,  4.6423e-01,  4.6423e-01,
          -1.0000e+09, -1.0000e+09]]], grad_fn=<MaskedFillBackward0>) 

【步骤3】计算注意力权重
注意力权重:
 tensor([[[0.1537, 0.2319, 0.1537, 0.2304, 0.2304, 0.0000, 0.0000],
         [0.2421, 0.2236, 0.2421, 0.1461, 0.1461, 0.0000, 0.0000],
         [0.1537, 0.2319, 0.1537, 0.2304, 0.2304, 0.0000, 0.0000],
         [0.1534, 0.1292, 0.1534, 0.2820, 0.2820, 0.0000, 0.0000],
         [0.1534, 0.1292, 0.1534, 0.2820, 0.2820, 0.0000, 0.0000],
         [0.2065, 0.1056, 0.2065, 0.2407, 0.2407, 0.0000, 0.0000],
         [0.2065, 0.1056, 0.2065, 0.2407, 0.2407, 0.0000, 0.0000]]],
       grad_fn=<SoftmaxBackward0>) 

【步骤 4】 计算加权和
最终加权和:
 tensor([[[-0.0252, -0.3829,  0.2684, -0.0838],
         [-0.2593, -0.3968,  0.0988,  0.0171],
         [-0.0252, -0.3829,  0.2684, -0.0838],
         [-0.1678, -0.3657,  0.2525, -0.0782],
         [-0.1678, -0.3657,  0.2525, -0.0782],
         [-0.3342, -0.3710,  0.1478, -0.0166],
         [-0.3342, -0.3710,  0.1478, -0.0166]]], grad_fn=<UnsafeViewBackward0>) 

在步骤 3 中,我们可以看到应用了 Padding Mask 之后,每个位置对填充位置(后两个位置)的注意力权重都被处理成了 0。

19.3. 因果注意力掩码(Causal Attention Mask)#

Causal Attention Mask 是一种用于在自注意力机制中防止模型“看到”未来信息的掩码技术。具体来说,它通过生成一个下三角矩阵来实现,该矩阵的上三角部分为 0,而对角线及其以下部分为 1。这个矩阵的作用是将自注意力计算中的权重设置为 0,从而确保在时间步 t 的解码输出只能依赖于 t 时刻之前的输入,而不能依赖于 t 之后的信息。

<bos>101<eos> 为例,它的因果注意力掩码矩阵如下:

image.png

在实际应用中,因果注意力掩码主要用于 Transformer 模型中的解码器部分。例如,在生成序列时,通过将当前位置之后的信息权重设置为负无穷大(或直接设置为 0),可以有效地阻止模型访问这些信息。这种机制确保了模型在生成每个新词时,只能基于已生成的词进行预测,从而保证了生成序列的自回归性质。

因果注意力掩码的实现通常涉及以下步骤:

  1. 生成掩码矩阵:根据序列长度生成一个形状为 [batch_size, seq_len, seq_len] 的下三角矩阵。

  2. 应用掩码:将该掩码矩阵与缩放点积注意力(scaled dot-product attention)的结果相乘,以屏蔽掉未来信息的影响。

19.3.1. 因果注意力掩码的代码实现#

import torch
import numpy as np


# 生成因果注意力掩码矩阵
def create_causal_attn_mask(input_ids):
    # 生成解码时因果注意力掩码的下三角矩阵,[batch_size, tgt_len, tgt_len]
    causal_attn_shape = [input_ids.size(0), input_ids.size(1), input_ids.size(1)]
    causal_attn_mask = np.tril(np.ones(causal_attn_shape), k=0)
    causal_attn_mask = torch.from_numpy(causal_attn_mask).byte()
    return causal_attn_mask

19.3.2. 生成因果注意力掩码#

answer = f"{tokenizer.bos_token}101{tokenizer.eos_token}"

# 对数据进行分词编码
encoded_result = tokenizer(answer, max_length=5, padding=True, return_tensors=True)

# 生成数据的嵌入表示
decoder_input_ids = encoded_result["input_ids"].unsqueeze(0)
decoder_input = embed_layer(decoder_input_ids)

# 生成因果注意力掩码矩阵
causal_attn_mask = create_causal_attn_mask(decoder_input_ids)

print("label_input_ids:\n", decoder_input_ids, "\n")
print("label_input:\n", decoder_input, "\n")
print("causal_attn_mask:\n", causal_attn_mask, "\n")
label_input_ids:
 tensor([[14,  1,  0,  1, 15]]) 

label_input:
 tensor([[[ 0.4236,  0.7995, -0.6115, -2.4611],
         [ 0.1085,  1.6926,  1.0728,  0.5288],
         [-1.6693, -0.1664,  0.6599,  0.1362],
         [ 0.1085,  1.6926,  1.0728,  0.5288],
         [ 0.1950,  0.5887, -2.0544, -0.5407]]], grad_fn=<EmbeddingBackward0>) 

causal_attn_mask:
 tensor([[[1, 0, 0, 0, 0],
         [1, 1, 0, 0, 0],
         [1, 1, 1, 0, 0],
         [1, 1, 1, 1, 0],
         [1, 1, 1, 1, 1]]], dtype=torch.uint8) 

19.3.3. 在 Masked-Attention 层中使用因果注意力掩码#

Masked-Attention 层本质上还是一个自注意力层,只是它在计算注意力权重时,使用的是因果注意力掩码。

# 使用单头自注意力层进行计算
hidden_states, _ = attn(q_input=decoder_input, k_input=decoder_input, v_input=decoder_input, attn_mask=causal_attn_mask)
【步骤1】计算query、key和value向量 

【步骤2】计算注意力分数
掩码前的scores:
 tensor([[[-0.2059,  0.2741,  0.3620,  0.2741, -0.2577],
         [-0.1440, -0.1930,  0.2117, -0.1930, -0.3016],
         [-0.0952,  0.0169,  0.6253,  0.0169, -0.5253],
         [-0.1440, -0.1930,  0.2117, -0.1930, -0.3016],
         [-0.1689,  0.1151,  0.0835,  0.1151, -0.1969]]],
       grad_fn=<DivBackward0>) 

【步骤 2-2】根据掩码重设无需关注的注意力分数
掩码后的scores:
 tensor([[[-2.0588e-01, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09],
         [-1.4405e-01, -1.9301e-01, -1.0000e+09, -1.0000e+09, -1.0000e+09],
         [-9.5159e-02,  1.6892e-02,  6.2533e-01, -1.0000e+09, -1.0000e+09],
         [-1.4405e-01, -1.9301e-01,  2.1167e-01, -1.9301e-01, -1.0000e+09],
         [-1.6894e-01,  1.1514e-01,  8.3462e-02,  1.1514e-01, -1.9687e-01]]],
       grad_fn=<MaskedFillBackward0>) 

【步骤3】计算注意力权重
注意力权重:
 tensor([[[1.0000, 0.0000, 0.0000, 0.0000, 0.0000],
         [0.5122, 0.4878, 0.0000, 0.0000, 0.0000],
         [0.2396, 0.2680, 0.4924, 0.0000, 0.0000],
         [0.2309, 0.2198, 0.3295, 0.2198, 0.0000],
         [0.1690, 0.2245, 0.2175, 0.2245, 0.1644]]],
       grad_fn=<SoftmaxBackward0>) 

【步骤 4】 计算加权和
最终加权和:
 tensor([[[ 0.1335,  0.7496, -0.7074,  0.3390],
         [-0.5268,  0.1869, -0.5712,  0.3311],
         [-0.2761, -0.0851, -0.0261,  0.0470],
         [-0.4930, -0.1093, -0.1788,  0.1394],
         [-0.4624, -0.1587, -0.3691,  0.2763]]], grad_fn=<UnsafeViewBackward0>) 

19.4. 交叉注意力掩码(Cross-Attention Mask)#

在 Transformer Decoder 的交叉注意力层中,查询(Query)来自 Decoder,键(Key)和值(Value)来自 Encoder 的输出。为了过滤编码器输出的填充部分(如补零位置),确保解码器仅关注编码器输出的实际内容,忽略填充位置的噪声。所以,需要生成一个交叉注意力掩码(Cross-Attention Mask)用于屏蔽编码器输出中的填充位置。

1+100=101 为例,编码器的输入为 [1, +, 1, 0, 0, Pad, Pad],解码器的输入为 [Bos,1, 0, 1, Eos]。交叉注意力掩码矩阵如下图所示:

image.png

生成交叉注意力掩码矩阵和生成填充掩码的代码是一样的,只是 Query 和 Key 的不同。生成填充掩码时,Query 和 Key 都是编码器的输入;而生成交叉注意力掩码时,Query 是解码器的输入,Key 是编码器的输入。

19.4.1. 生成交叉注意力掩码#

# 编码器的输入:question = "1+100",使用分词器填充了两个Token,总长度为7
# 解码器的输入:answer = f"{tokenizer.bos_token}101{tokenizer.eos_token}",总长度为5
cross_attn_pad_mask = create_cross_attn_pad_mask(query_ids=decoder_input_ids,
                                                 key_ids=encoder_input_ids,
                                                 pad_token_id=pad_token_id)

print_red(f"交叉注意力掩码:{cross_attn_pad_mask.shape}")
print(cross_attn_pad_mask)
交叉注意力掩码:torch.Size([1, 5, 7])
tensor([[[1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0, 0]]], dtype=torch.uint8)

19.4.2. 在 Cross-Attention 层中使用交叉注意力掩码#

Cross-Attention 层本质上也是一个自注意力层,只是它在计算时,使用的 Query、Key、Value 来源以及掩码不同而已。

自注意力层的 Query、Key、Value 都是来自同一个输入,而 Cross-Attention 层的 Query 来自解码器,Key、Value 来自编码器。另外,使用交叉注意力掩码是解码器生成的结果相对编码器输入的注意力。

# 使用单头自注意力层进行计算
hidden_states, _ = attn(q_input=decoder_input, k_input=encoder_input, v_input=encoder_input,
                        attn_mask=cross_attn_pad_mask)
【步骤1】计算query、key和value向量 

【步骤2】计算注意力分数
掩码前的scores:
 tensor([[[ 0.2741, -0.2035,  0.2741,  0.3620,  0.3620, -0.0509, -0.0509],
         [-0.1930,  0.2184, -0.1930,  0.2117,  0.2117,  0.2241,  0.2241],
         [ 0.0169, -0.1553,  0.0169,  0.6253,  0.6253,  0.4403,  0.4403],
         [-0.1930,  0.2184, -0.1930,  0.2117,  0.2117,  0.2241,  0.2241],
         [ 0.1151, -0.0567,  0.1151,  0.0835,  0.0835, -0.0904, -0.0904]]],
       grad_fn=<DivBackward0>) 

【步骤 2-2】根据掩码重设无需关注的注意力分数
掩码后的scores:
 tensor([[[ 2.7411e-01, -2.0353e-01,  2.7411e-01,  3.6203e-01,  3.6203e-01,
          -1.0000e+09, -1.0000e+09],
         [-1.9301e-01,  2.1841e-01, -1.9301e-01,  2.1167e-01,  2.1167e-01,
          -1.0000e+09, -1.0000e+09],
         [ 1.6892e-02, -1.5526e-01,  1.6892e-02,  6.2533e-01,  6.2533e-01,
          -1.0000e+09, -1.0000e+09],
         [-1.9301e-01,  2.1841e-01, -1.9301e-01,  2.1167e-01,  2.1167e-01,
          -1.0000e+09, -1.0000e+09],
         [ 1.1514e-01, -5.6705e-02,  1.1514e-01,  8.3462e-02,  8.3462e-02,
          -1.0000e+09, -1.0000e+09]]], grad_fn=<MaskedFillBackward0>) 

【步骤3】计算注意力权重
注意力权重:
 tensor([[[0.2082, 0.1291, 0.2082, 0.2273, 0.2273, 0.0000, 0.0000],
         [0.1537, 0.2319, 0.1537, 0.2304, 0.2304, 0.0000, 0.0000],
         [0.1534, 0.1292, 0.1534, 0.2820, 0.2820, 0.0000, 0.0000],
         [0.1537, 0.2319, 0.1537, 0.2304, 0.2304, 0.0000, 0.0000],
         [0.2092, 0.1762, 0.2092, 0.2027, 0.2027, 0.0000, 0.0000]]],
       grad_fn=<SoftmaxBackward0>) 

【步骤 4】 计算加权和
最终加权和:
 tensor([[[-0.3056, -0.3752,  0.1483, -0.0160],
         [-0.0252, -0.3829,  0.2684, -0.0838],
         [-0.1678, -0.3657,  0.2525, -0.0782],
         [-0.0252, -0.3829,  0.2684, -0.0838],
         [-0.2427, -0.3832,  0.1538, -0.0175]]], grad_fn=<UnsafeViewBackward0>) 

从第 3 步中,我们可以看到,应用了交叉注意力掩码之后,解码器的每个位置只关注编码器输入中有效的部分,而忽略了填充位置。这样,就不会从输入的填充位置中引入噪声,从而提高模型的性能。

image.png

19.5. 本章小结#

本章深入探讨了 Transformer 模型中的解码器模块,并动手实现了它的核心组件。详细了解了 Transformer 中使用的 3 种注意力机制:填充掩码、因果注意力掩码和交叉注意力掩码。在下一章,我们将继续动手实现完整的 Transformer 模型。

19.6. 答疑讨论#